﻿using DevExpress.Data;
using DevExpress.Data.Filtering;
using DevExpress.Data.Filtering.Helpers;
using DevExpress.Data.Linq.Helpers;
using DevExpress.Data.ODataLinq;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.DC;
using DevExpress.ExpressApp.Filtering;
using DevExpress.ExpressApp.Utils;
using DevExpress.ExpressApp.Utils.Reflection;
using DevExpress.Xpo;
using Microsoft.OData.Client;
using System.Collections;
using System.ComponentModel;
using System.Linq.Expressions;

namespace EasyXaf.ObjectSpaces.OData;

public class ODataObjectSpace : CompositeObjectSpace, IODataObjectSpace
{
    private readonly Uri _serviceRoot;
    private readonly IServiceProvider _serviceProvider;

    private ODataContext _odataContext;

    public override IServiceProvider ServiceProvider => _serviceProvider;

    public Uri ServiceRoot => _serviceRoot;

    public DataServiceContext ODataContext => _odataContext;

    public override IList ModifiedObjects => GetModifiedObjects();

    public ODataObjectSpace(IServiceProvider serviceProvider, ITypesInfo typesInfo, Uri serviceRoot)
            : this(serviceProvider, typesInfo, (typesInfo as TypesInfo)?.FindEntityStore(typeof(ODataTypeInfoSource)), serviceRoot)
    {
    }

    public ODataObjectSpace(IServiceProvider serviceProvider, ITypesInfo typesInfo, IEntityStore entityStore, Uri serviceRoot)
        : base(typesInfo, entityStore)
    {
        _serviceRoot = serviceRoot;
        _serviceProvider = serviceProvider;
        _odataContext = new ODataContext(serviceRoot, this);
    }

    private static void ThrowTypeNotRegisteredException(Type objectType)
    {
        throw new ArgumentException($"\"{objectType.FullName}\"没有被注册，如果是 odata 对象类型，请添加 DomainComponentAttribute 特性，并继承 ODataObject<TKey> 类");
    }

    private IList GetModifiedObjects()
    {
        return GetModifiedObjects(IsStateToSave);
    }

    private IList GetModifiedObjects(Predicate<EntityStates> statePredicate)
    {
        CheckIsDisposed();

        var entityTracker = _odataContext.EntityTracker;
        return new List<object>(GetModifiedObjectsCore(entityTracker.Entities, statePredicate));
    }

    private static IEnumerable<object> GetModifiedObjectsCore(IEnumerable<EntityDescriptor> entries, Predicate<EntityStates> statePredicate)
    {
        foreach (var entry in entries)
        {
            if (statePredicate(entry.State))
            {
                yield return entry.Entity;
            }
        }
    }

    private bool IsStateToSave(EntityStates state)
    {
        return (state & EntityStates.Added) == EntityStates.Added
            || (state & EntityStates.Deleted) == EntityStates.Deleted
            || (state & EntityStates.Modified) == EntityStates.Modified;
    }

    private void AddObjectSpaceLink(IObjectSpaceLink obj)
    {
        if (obj != null && obj.ObjectSpace == null)
        {
            obj.ObjectSpace = this;
        }
    }

    protected virtual void SubscribeToObjectEvents(object obj)
    {
        if (obj is INotifyPropertyChanged notifyPropertyChanged)
        {
            notifyPropertyChanged.PropertyChanged += Object_PropertyChanged;
        }
    }

    protected virtual void UnsubscribeFromObjectEvents(object obj)
    {
        if (obj is INotifyPropertyChanged notifyPropertyChanged)
        {
            notifyPropertyChanged.PropertyChanged -= Object_PropertyChanged;
        }
    }

    public void AcceptObject(object obj)
    {
        if (obj != null)
        {
            UnsubscribeFromObjectEvents(obj);
            SubscribeToObjectEvents(obj);
            AddObjectSpaceLink(obj as IObjectSpaceLink);
        }
    }

    protected override IList CreateCollection(Type objectType, CriteriaOperator criteria, IList<SortProperty> sorting, bool inTransaction)
    {
        IList list;

        if (IsKnownType(objectType))
        {
            list = new ODataCollection(this, objectType, criteria, sorting);
        }
        else
        {
            list = base.CreateCollection(objectType, criteria, sorting, inTransaction);
        }

        if (list == null)
        {
            ThrowTypeNotRegisteredException(objectType);
        }

        return list;
    }

    protected override IList<T> CreateCollection<T>(CriteriaOperator criteria, IList<SortProperty> sorting, bool inTransaction)
    {
        IList<T> list;

        if (IsKnownType(typeof(T)))
        {
            list = new ODataCollection<T>(this, criteria, sorting);
        }
        else
        {
            list = base.CreateCollection<T>(criteria, sorting, inTransaction);
        }

        if (list == null)
        {
            ThrowTypeNotRegisteredException(typeof(T));
        }

        return list;
    }

    public override object CreateServerCollection(Type objectType, CriteriaOperator criteria)
    {
        object obj;

        if (IsKnownType(objectType))
        {
            obj = new ODataServerModeSource
            {
                Query = GetObjectsQuery(objectType, criteria)
            };
        }
        else
        {
            obj = base.CreateServerCollection(objectType, criteria);
        }

        if (obj == null)
        {
            ThrowTypeNotRegisteredException(objectType);
        }

        return obj;
    }

    public IList<T> CreateCollection<T>(CriteriaOperator criteria, object masterObject, string propertyName)
    {
        return new ODataCollection<T>(this, criteria, null, masterObject, propertyName);
    }

    protected override object CreateObjectCore(Type objectType)
    {
        object obj = null;
        if (IsKnownType(objectType))
        {
            obj = TypeHelper.CreateInstance(objectType);
            _odataContext.AddObject(obj.GetType().GetEntitySetName(), obj);
            AcceptObject(obj);
        }
        return obj;
    }

    public override object CreateObject(Type objectType)
    {
        var obj = base.CreateObject(objectType);
        if (obj == null)
        {
            ThrowTypeNotRegisteredException(objectType);
        }
        return obj;
    }

    public override IDisposable CreateParseCriteriaScope()
    {
        return new ODataParseCriteriaScope(this);
    }

    protected override void DeleteCore(IList objects)
    {
        if (objects.Count > 0)
        {
            DeleteCore(objects, list =>
            {
                foreach (var obj in list)
                {
                    _odataContext.DeleteObject(obj);
                }
            }, raiseEvents: true);
        }
    }

    protected override void DoCommit()
    {
        isCommitting = false;

        try
        {
            var modifiedObjects = GetModifiedObjects();

            foreach (var modifiedObject in modifiedObjects)
            {
                OnObjectSaving(modifiedObject);
                if (modifiedObject is IXafEntityObject entityObject)
                {
                    entityObject.OnSaving();
                }
            }

            base.DoCommit();

            if (_odataContext.EntityTracker.Entities.Any())
            {
                _odataContext.SaveChanges();
            }

            foreach (var modifiedObject in modifiedObjects)
            {
                OnObjectSaved(modifiedObject);
            }
        }
        finally
        {
            isCommitting = false;
        }
    }

    public override void EnableObjectDeletionOnRemove(object collection, bool enable)
    {
        if (collection is ODataCollection odataCollection)
        {
            odataCollection.DeleteObjectOnRemove = enable;
        }
        else
        {
            base.EnableObjectDeletionOnRemove(collection, enable);
        }
    }

    public override object Evaluate(Type objectType, CriteriaOperator expression, CriteriaOperator criteria)
    {
        var query = GetObjectsQuery(objectType, criteria);
        foreach (var obj in query.Take(1))
        {
            AcceptObject(obj);
            return obj;
        }
        return null;
    }

    public override EvaluatorContextDescriptor GetEvaluatorContextDescriptor(Type type)
    {
        if (IsKnownType(type))
        {
            return new XafEvaluatorContextDescriptor(typesInfo.FindTypeInfo(type), this);
        }
        return base.GetEvaluatorContextDescriptor(type);
    }

    public override object FindObject(Type objectType, CriteriaOperator criteria, bool inTransaction)
    {
        if (IsKnownType(objectType))
        {
            foreach (var obj in GetObjectsQuery(objectType, criteria))
            {
                AcceptObject(obj);
                return obj;
            }
        }
        return base.FindObject(objectType, criteria, inTransaction);
    }

    public override TObjectType FirstOrDefault<TObjectType>(Expression<Func<TObjectType, bool>> criteriaExpression, bool inTransaction)
    {
        TObjectType obj = default;

        if (IsKnownType(typeof(TObjectType)))
        {
            var query = GetObjectsQuery(typeof(TObjectType), CriteriaOperator.FromLambda(criteriaExpression)).Take(1);
            foreach (var item in query)
            {
                obj = (TObjectType)item;
            }
            AcceptObject(obj);
        }
        else
        {
            obj = base.FirstOrDefault(criteriaExpression, inTransaction);
        }

        return obj;
    }

    protected override void SetModified(object obj, ObjectChangedEventArgs args)
    {
        if (obj != null && IsKnownType(obj.GetType()))
        {
            var entityDescriptor = _odataContext.GetEntityDescriptor(obj);
            if (entityDescriptor == null)
            {
                _odataContext.AttachTo(obj.GetType().GetEntitySetName(), obj);
            }
            OnObjectChanged(args);
            SetIsModified(true);
        }
        else
        {
            base.SetModified(obj, args);
        }
    }

    protected override void OnObjectChanged(ObjectChangedEventArgs args)
    {
        _odataContext.UpdateObject(args.Object);
        base.OnObjectChanged(args);
    }

    protected override void ReloadCore(Action body, Action finalizer)
    {
        isReloading = true;
        try
        {
            base.ReloadCore(() =>
            {
                body?.Invoke();
                SetIsModified(false);

                _odataContext?.Dispose();
                _odataContext = new ODataContext(_serviceRoot, this);

            }, finalizer);
        }
        finally
        {
            isReloading = false;
        }
    }

    public override object ReloadObject(object obj)
    {
        if (obj != null && IsKnownType(obj.GetType()))
        {
            var entityDescriptor = _odataContext.GetEntityDescriptor(obj);
            if (entityDescriptor != null)
            {
                if (entityDescriptor.State == EntityStates.Unchanged || entityDescriptor.State == EntityStates.Modified)
                {
                    if (obj is ODataObject odataObject)
                    {
                        odataObject.Reload();
                        return obj;
                    }
                }
            }
        }
        return base.ReloadObject(obj);
    }

    public override bool Contains(object obj)
    {
        Guard.ArgumentNotNull(obj, nameof(obj));

        if (IsKnownType(obj.GetType()))
        {
            CheckIsDisposed();

            if (obj is IObjectSpaceLink objectSpaceLink && objectSpaceLink.ObjectSpace != null)
            {
                return objectSpaceLink.ObjectSpace == this;
            }

            return true;
        }

        return base.Contains(obj);
    }

    public override IObjectSpace CreateNestedObjectSpace()
    {
        return new ODataObjectSpace(ServiceProvider, typesInfo, entityStore, _serviceRoot);
    }

    public override object GetObject(object obj)
    {
        CheckIsDisposed();

        object result;

        if (obj != null && IsKnownType(obj.GetType()))
        {
            var entityDescriptor = _odataContext.GetEntityDescriptor(obj);
            if (entityDescriptor != null && entityDescriptor.State != EntityStates.Detached)
            {
                result = obj;
                AcceptObject(obj);
            }
            else
            {
                result = GetObjectByKey(obj.GetType(), GetKeyValue(obj));
            }
        }
        else
        {
            result = base.GetObject(obj);
        }

        return result;
    }

    public override object GetObjectByKey(Type objectType, object key)
    {
        CheckIsDisposed();

        object obj = null;

        if (IsKnownType(objectType))
        {
            var typeInfo = TypesInfo.FindTypeInfo(GetOriginalType(objectType));
            if (typeInfo.KeyMember != null)
            {
                foreach (var item in GetObjectsQuery(objectType, new BinaryOperator(typeInfo.KeyMember.Name, key)).Take(1))
                {
                    obj = item;
                }
                AcceptObject(obj);
            }
        }
        else
        {
            obj = base.GetObjectByKey(objectType, key);
        }

        return obj;
    }

    public override int GetObjectsCount(Type objectType, CriteriaOperator criteria)
    {
        if (IsKnownType(objectType))
        {
            return GetObjectsQuery(objectType, criteria).Count();
        }
        return base.GetObjectsCount(objectType, criteria);
    }

    public IQueryable GetObjectsQuery(Type objectType, CriteriaOperator criteria = null, SortProperty[] sorting = null)
    {
        var objectTypeInfo = TypesInfo.FindTypeInfo(objectType);
        var expandPaths = objectTypeInfo.FindAttributes<ODataExpandAttribute>().Select(e => e.Path).ToList();
        foreach (var memberInfo in objectTypeInfo.Members)
        {
            if (memberInfo.IsPersistent && memberInfo.IsAssociation && !memberInfo.IsDelayed && !memberInfo.IsList)
            {
                expandPaths.Add(memberInfo.FindAttribute<OriginalNameAttribute>()?.OriginalName ?? memberInfo.Name);
            }
        }
        return new ODataQueryableProvider(_odataContext, objectType, criteria, sorting, expandPaths.ToArray()).Queryable;
    }

    public override IQueryable<T> GetObjectsQuery<T>(bool inTransaction = false)
    {
        if (IsKnownType(typeof(T)))
        {
            return _odataContext.CreateQuery<T>(typeof(T).GetEntitySetName());
        }
        else
        {
            return base.GetObjectsQuery<T>(inTransaction);
        }
    }

    public override bool CanApplyCriteria(Type collectionType)
    {
        if (typeof(ODataCollection).IsAssignableFrom(collectionType) || typeof(ODataServerModeSource).IsAssignableFrom(collectionType))
        {
            return true;
        }
        return base.CanApplyCriteria(collectionType);
    }

    protected override bool? CanApplyCriteriaCore(object collection, CriteriaOperator criteria)
    {
        return true;
    }

    protected override bool? CanApplySortingCore(object collection, CriteriaOperator criteria)
    {
        return true;
    }

    public override bool IsNewObject(object obj)
    {
        CheckIsDisposed();

        if (obj != null && IsKnownType(obj.GetType()))
        {
            return _odataContext.GetEntityDescriptor(obj)?.State == EntityStates.Added;
        }

        return false;
    }

    public override bool IsDeletedObject(object obj)
    {
        CheckIsDisposed();

        if (obj != null && IsKnownType(obj.GetType()))
        {
            return _odataContext.GetEntityDescriptor(obj)?.State == EntityStates.Detached;
        }

        return false;
    }

    public override bool IsObjectToSave(object obj)
    {
        CheckIsDisposed();

        if (obj != null && IsKnownType(obj.GetType()))
        {
            var entityDescriptor = _odataContext.GetEntityDescriptor(obj);
            if (entityDescriptor != null)
            {
                return IsStateToSave(entityDescriptor.State);
            }
        }

        return false;
    }

    public override bool IsObjectToDelete(object obj)
    {
        CheckIsDisposed();

        if (obj != null && IsKnownType(obj.GetType()))
        {
            return _odataContext.GetEntityDescriptor(obj)?.State == EntityStates.Deleted;
        }

        return false;
    }

    public override bool IsObjectDeletionOnRemoveEnabled(object collection)
    {
        if (collection is ODataCollection odataCollection)
        {
            return odataCollection.DeleteObjectOnRemove;
        }
        return base.IsObjectDeletionOnRemoveEnabled(collection);
    }

    public override bool IsCollectionLoaded(object collection)
    {
        if (collection is ODataCollection odataCollection)
        {
            return odataCollection.IsLoaded;
        }
        else if (collection is ODataServerModeSource)
        {
            return true;
        }
        return base.IsCollectionLoaded(collection);
    }

    public override ICollection GetObjectsToSave(bool includeParent)
    {
        return GetModifiedObjects();
    }

    public override ICollection GetObjectsToDelete(bool includeParent)
    {
        return GetModifiedObjects(state => state == EntityStates.Deleted);
    }

    public override Type GetCollectionObjectType(object collection)
    {
        if (collection is ODataCollection odataCollection)
        {
            return odataCollection.ObjectType;
        }
        else if (collection is ODataServerModeSource odataServerModeSource)
        {
            return odataServerModeSource.ElementType;
        }
        return base.GetCollectionObjectType(collection);
    }

    public override void ApplyCriteria(object collection, CriteriaOperator criteria)
    {
        if (collection is ODataCollection odataCollection)
        {
            odataCollection.Criteria = criteria;
        }
        else if (collection is ODataServerModeSource odataServerModeSource)
        {
            odataServerModeSource.FixedFilterCriteria = criteria;
        }
        else
        {
            base.ApplyCriteria(collection, criteria);
        }
    }

    public override CriteriaOperator GetCriteria(object collection)
    {
        if (collection is ODataCollection odataCollection)
        {
            return odataCollection.Criteria;
        }
        else if (collection is ODataServerModeSource odataServerModeSource)
        {
            return odataServerModeSource.FixedFilterCriteria;
        }
        return base.GetCriteria(collection);
    }

    public override IList<SortProperty> GetCollectionSorting(object collection)
    {
        if (collection is ODataCollection odataCollection)
        {
            return odataCollection.Sorting;
        }
        else if (collection is ODataServerModeSource odataServerModeSource)
        {
            return ConvertStringToSorting(odataServerModeSource.DefaultSorting ?? string.Empty);
        }
        return base.GetCollectionSorting(collection);
    }

    public override void SetCollectionSorting(object collection, IList<SortProperty> sorting)
    {
        if (collection is ODataCollection odataCollection)
        {
            odataCollection.Sorting = sorting?.ToArray();
        }
        else if (collection is ODataServerModeSource odataServerModeSource)
        {
            odataServerModeSource.DefaultSorting = ConvertSortingToString(sorting);
        }
        else
        {
            base.SetCollectionSorting(collection, sorting);
        }
    }

    public override int GetTopReturnedObjectsCount(object collection)
    {
        if (collection is ODataCollection odataCollection)
        {
            return odataCollection.TopReturnedObjectsCount;
        }
        return base.GetTopReturnedObjectsCount(collection);
    }

    public override void SetTopReturnedObjectsCount(object collection, int topReturnedObjectsCount)
    {
        if (collection is ODataCollection odataCollection)
        {
            odataCollection.TopReturnedObjectsCount = topReturnedObjectsCount;
        }
        else
        {
            base.SetTopReturnedObjectsCount(collection, topReturnedObjectsCount);
        }
    }

    public override IListServer GetListServer(object collection)
    {
        if (collection is ODataServerModeSource)
        {
            return ((IListSource)collection).GetList() as IListServer;
        }
        return null;
    }

    public override void ReloadCollection(object collection)
    {
        if (collection is ODataCollection odataCollection)
        {
            odataCollection.Reload();
        }
        else if (collection is ODataServerModeSource odataServerModeSource)
        {
            odataServerModeSource.Reload();
        }
        else
        {
            base.ReloadCollection(collection);
        }
    }

    public override void RemoveFromModifiedObjects(object obj)
    {
        CheckIsDisposed();

        if (obj != null && IsKnownType(obj.GetType()))
        {
            var entityDescriptor = _odataContext.GetEntityDescriptor(obj);
            if (entityDescriptor != null && entityDescriptor.State != EntityStates.Unchanged)
            {
                _odataContext.ChangeState(obj, EntityStates.Unchanged);
            }
        }
    }

    public override void Dispose()
    {
        _odataContext?.Dispose();
        _odataContext = null;

        base.Dispose();
        isDisposed = true;

        GC.SuppressFinalize(this);
    }
}
